/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：太平金科-基础组件封装层-ElasticSearch-核心依赖
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taipingframework.boot.es.metadata;

import com.taipingframework.boot.es.toolkit.LambdaUtils;
import com.taipingframework.boot.es.toolkit.reflection.ClassUtils;
import com.taipingframework.boot.es.toolkit.reflection.ReflectionKit;
import com.taipingframework.utility.extend.StringHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

/**
 * <p>
 * 实体类反射表辅助类
 * </p>
 */
@Slf4j
public class TableInfoHelper {

    /**
     * 储存反射类表信息
     */
    private static final Map<Class<?>, TableInfo> TABLE_INFO_CACHE = new ConcurrentHashMap<>();
    /**
     * 储存表名对应的反射类表信息
     */
    private static final Map<String, TableInfo> TABLE_NAME_INFO_CACHE = new ConcurrentHashMap<>();

    /**
     * <p>
     * 获取实体映射表信息
     * </p>
     *
     * @param clazz 反射实体类
     * @return 数据库表反射信息
     */
    public static TableInfo getTableInfo(Class<?> clazz) {
        if (clazz == null || ReflectionKit.isPrimitiveOrWrapper(clazz) || clazz == String.class || clazz.isInterface()) {
            return null;
        }
        // https://github.com/baomidou/mybatis-plus/issues/299
        Class<?> targetClass = ClassUtils.getUserClass(clazz);
        TableInfo tableInfo = TABLE_INFO_CACHE.get(targetClass);
        if (null != tableInfo) {
            return tableInfo;
        }
        //尝试获取父类缓存
        Class<?> currentClass = clazz;
        while (null == tableInfo && Object.class != currentClass) {
            currentClass = currentClass.getSuperclass();
            tableInfo = TABLE_INFO_CACHE.get(ClassUtils.getUserClass(currentClass));
        }
        //把父类的移到子类中来
        if (tableInfo != null) {
            TABLE_INFO_CACHE.put(targetClass, tableInfo);
        }
        return tableInfo;
    }

    /**
     * <p>
     * 根据表名获取实体映射表信息
     * </p>
     *
     * @param tableName 表名
     * @return 数据库表反射信息
     */
    public static TableInfo getTableInfo(String tableName) {
        if (StringUtils.isBlank(tableName)) {
            return null;
        }
        return TABLE_NAME_INFO_CACHE.get(tableName);
    }

    /**
     * <p>
     * 实体类反射获取表信息【初始化】
     * </p>
     *
     * @param clazz 反射实体类
     */
    public static synchronized void initTableInfo(Class<?> clazz) {
        /* 没有获取到缓存信息,则初始化 */
        TableInfo tableInfo = new TableInfo(clazz);

        /* 初始化表名 */
        initTableName(clazz, tableInfo);

        /* 初始化字段相关 */
        initTableFields(clazz, tableInfo);

        TABLE_INFO_CACHE.put(clazz, tableInfo);
        TABLE_NAME_INFO_CACHE.put(tableInfo.getTableName(), tableInfo);

        /* 缓存 lambda */
        LambdaUtils.installCache(tableInfo);
    }

    /**
     * <p>
     * 初始化 表数据库类型,表名
     * </p>
     *
     * @param clazz     实体类
     * @param tableInfo 数据库表反射信息
     */
    private static void initTableName(Class<?> clazz, TableInfo tableInfo) {
        Document table = clazz.getAnnotation(Document.class);
        String tableName = clazz.getSimpleName();

        if (table != null) {
            if (StringUtils.isNotBlank(table.indexName())) {
                tableName = table.indexName();
            }
            tableInfo.setShards(table.shards());
            tableInfo.setReplicas(table.replicas());
        } else {
            tableName = StringHelper.camelToUnderline(tableName);
        }
        tableInfo.setTableName(tableName);
    }

    /**
     * 初始化 表主键,表字段
     *
     * @param clazz     实体类
     * @param tableInfo 数据库表反射信息
     */
    private static void initTableFields(Class<?> clazz, TableInfo tableInfo) {
        List<Field> list = getAllFields(ClassUtils.getUserClass(clazz));
        // 标记是否读取到主键
        boolean isReadPK = false;
        // 是否存在 @Id 注解
        boolean existId = list.stream().anyMatch(field -> field.isAnnotationPresent(Id.class));

        List<TableFieldInfo> fieldList = new ArrayList<>(list.size());
        for (Field field : list) {
            /* 主键ID 初始化 */
            if (existId) {
                Id id = field.getAnnotation(Id.class);
                if (id != null) {
                    if (isReadPK) {
                        throw new RuntimeException(String.format("@Id can't more than one in Class: \"%s\".", clazz.getName()));
                    } else {
                        // 根据@Id注解初始化ID字段
                        initTableIdWithAnnotation(tableInfo, field);
                        isReadPK = true;
                        continue;
                    }
                }
            } else if (!isReadPK) {
                // 根据字段名初始化ID字段
                isReadPK = initTableIdWithoutAnnotation(tableInfo, field);
                if (isReadPK) {
                    continue;
                }
            }

            final org.springframework.data.elasticsearch.annotations.Field docField =
                    field.getAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);

            /* 有 @Field 注解的字段初始化 */
            if (docField != null) {
                fieldList.add(new TableFieldInfo(field, docField));
                continue;
            }

            /* 无 @Field 注解的字段初始化 */
            fieldList.add(new TableFieldInfo(field));
        }

        /* 字段列表 */
        tableInfo.setFieldList(fieldList);

        /* 未发现主键注解，提示警告信息 */
        if (!isReadPK) {
            log.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName()));
        }
    }

    /**
     * <p>
     * 获取该类的所有属性列表
     * </p>
     *
     * @param clazz 反射类
     * @return 属性集合
     */
    private static List<Field> getAllFields(Class<?> clazz) {
        Field[] fields = clazz.getDeclaredFields();

        List<Field> superFields = new ArrayList<>();
        Class<?> currentClass = clazz.getSuperclass();
        while (currentClass != null) {
            Field[] declaredFields = currentClass.getDeclaredFields();
            Collections.addAll(superFields, declaredFields);
            currentClass = currentClass.getSuperclass();
        }
        /* 排除重载属性 */
        Map<String, Field> fieldMap = excludeOverrideSuperField(fields, superFields);
        return fieldMap.values()
                .stream()
                /* 过滤静态属性 */
                .filter(f -> !Modifier.isStatic(f.getModifiers()))
                /* 过滤 transient关键字修饰的属性 */
                .filter(f -> !Modifier.isTransient(f.getModifiers()))
                /* 过滤注解非表字段属性 */
                .filter(field -> field.getAnnotation(Id.class) != null
                        || field.getAnnotation(org.springframework.data.elasticsearch.annotations.Field.class) != null
                )
                .collect(toList());
    }

    /**
     * <p>
     * 排序重置父类属性
     * </p>
     *
     * @param fields         子类属性
     * @param superFieldList 父类属性
     */
    private static Map<String, Field> excludeOverrideSuperField(Field[] fields, List<Field> superFieldList) {
        // 子类属性
        Map<String, Field> fieldMap = Stream.of(fields).collect(toMap(Field::getName, identity(),
                (u, v) -> {
                    throw new IllegalStateException(String.format("Duplicate key %s", u));
                },
                LinkedHashMap::new));
        superFieldList.stream().filter(field -> !fieldMap.containsKey(field.getName()))
                .forEach(f -> fieldMap.put(f.getName(), f));
        return fieldMap;
    }

    /**
     * <p>
     * 主键属性初始化
     * </p>
     *
     * @param tableInfo 表信息
     * @param field     字段
     */
    private static void initTableIdWithAnnotation(TableInfo tableInfo, Field field) {
        final String property = field.getName();
        if (field.getAnnotation(org.springframework.data.elasticsearch.annotations.Field.class) != null) {
            log.warn(String.format("This \"%s\" is the table primary key by @TableId annotation in Class: \"%s\",So @Field annotation will not work!",
                    property, tableInfo.getEntityType().getName()));
        }

        final Class<?> keyType = field.getType();
        if (keyType.isPrimitive()) {
            log.warn(String.format("This primary key of \"%s\" is primitive !不建议如此请使用包装类 in Class: \"%s\"", property, tableInfo.getEntityType().getName()));
        }
        tableInfo.setKeyColumn(property)
                .setKeyProperty(property)
                .setKeyType(keyType);
    }

    /**
     * <p>
     * 主键属性初始化
     * </p>
     *
     * @param tableInfo 表信息
     * @param field     字段
     * @return true 继续下一个属性判断，返回 continue;
     */
    private static boolean initTableIdWithoutAnnotation(TableInfo tableInfo, Field field) {
        final String property = field.getName();
        if ("id".equalsIgnoreCase(property)) {
            if (field.getAnnotation(org.springframework.data.elasticsearch.annotations.Field.class) != null) {
                log.warn(String.format("This \"%s\" is the table primary key by default name for `id` in Class: \"%s\",So @Field will not work!",
                        property, tableInfo.getEntityType().getName()));
            }
            final Class<?> keyType = field.getType();
            if (keyType.isPrimitive()) {
                log.warn(String.format("This primary key of \"%s\" is primitive !不建议如此请使用包装类 in Class: \"%s\"",
                        property, tableInfo.getEntityType().getName()));
            }
            tableInfo.setKeyColumn(property)
                    .setKeyProperty(property)
                    .setKeyType(keyType);
            return true;
        }
        return false;
    }

}
